import { camelCase, pascalCase } from 'change-case';
import ejs from 'ejs';
import { promises as fs } from 'fs';
import inquirer from 'inquirer';
import { createConnection } from 'mysql2/promise';
import { join } from 'path';
import config from './config';

type TableStructType = {
  Field: string;
  Type: string;
  Null: string;
  Key: string;
  Default: any;
  Extra: string;
};

type SeqType =
  | 'STRING'
  | 'CHAR'
  | 'TEXT'
  | 'NUMBER'
  | 'TINYINT'
  | 'SMALLINT'
  | 'MEDIUMINT'
  | 'INTEGER'
  | 'BIGINT'
  | 'FLOAT'
  | 'DOUBLE'
  | 'DECIMAL'
  | 'BOOLEAN'
  | 'TIME'
  | 'DATE';

type FormattedTableStructType = {
  field: string;
  primaryKey?: string;
  originType: string;
  typeSize?: number;
  seqType: SeqType; // sequelize
  ssType?: string; // superstruct
  type: string; // typescript
  nullable: boolean;
  primary: boolean;
  unique: boolean;
  autoIncrement: boolean;
};

function parseType(fieldType: string): {
  originType: string;
  typeSize?: number;
  seqType: SeqType;
  ssType?: string;
  type: string;
} {
  const lowerType = fieldType.toLowerCase();
  const sizeMatch = /\w+\((\d+)\)/.exec(lowerType);
  const size = sizeMatch ? Number(sizeMatch[1]) : undefined;
  if (lowerType.startsWith('int(')) {
    return {
      originType: lowerType,
      seqType: 'NUMBER',
      ssType: 'number',
      type: 'number',
    };
  }
  if (lowerType.startsWith('bigint(')) {
    return {
      originType: lowerType,
      seqType: 'BIGINT',
      ssType: 'number',
      type: 'number',
    };
  }
  if (lowerType.startsWith('date')) {
    return {
      originType: lowerType,
      seqType: 'DATE',
      ssType: 'date',
      type: 'Date',
    };
  }
  if (lowerType.startsWith('varchar(')) {
    return {
      originType: lowerType,
      typeSize: size,
      seqType: 'STRING',
      ssType: 'string',
      type: 'string',
    };
  }
  if (lowerType.endsWith('text')) {
    return {
      originType: lowerType,
      seqType: 'TEXT',
      ssType: 'string',
      type: 'string',
    };
  }

  throw new Error(`Unknown Type '${fieldType}'`);
}

(async () => {
  const connection = await createConnection(config.mysql);

  const [result, field] = await connection.query('show tables');

  let tableList: string[] = [];
  if (Array.isArray(result)) {
    tableList = result.map(obj => obj[field[0].name]);
  }

  const controllerSet = new Set<string>();
  const serviceSet = new Set<string>();
  const modelSet = new Set<string>();

  const { controllerList } = await inquirer.prompt({
    type: 'checkbox',
    name: 'controllerList',
    choices: tableList,
    message: '请选择需要生成 Controller 的表',
  });

  controllerList.forEach((name) => {
    controllerSet.add(name);
  });
  controllerList.forEach((name) => {
    serviceSet.add(name);
  });
  controllerList.forEach((name) => {
    modelSet.add(name);
  });

  if (tableList.length > serviceSet.size) {
    const { serviceList } = await inquirer.prompt({
      type: 'checkbox',
      name: 'serviceList',
      choices: tableList.filter(name => !serviceSet.has(name)),
      message: '请选择需要生成 Service 的表',
    });
    serviceList.forEach((name) => {
      serviceSet.add(name);
    });
    serviceList.forEach((name) => {
      modelSet.add(name);
    });
  }

  if (tableList.length > modelSet.size) {
    const { modelList } = await inquirer.prompt({
      type: 'checkbox',
      name: 'modelList',
      choices: tableList.filter(name => !modelSet.has(name)),
      message: '请选择需要生成 Model 的表',
    });
    modelList.forEach((name) => {
      modelSet.add(name);
    });
  }

  const tableStruct = await Promise.all(
    [...modelSet].map(async (tableName) => {
      const [result] = await connection.query(`desc ${tableName}`);
      const struct = (
        result as TableStructType[]
      ).map<FormattedTableStructType>(field => ({
        field: field.Field,
        ...parseType(field.Type),
        nullable: field.Null === 'YES',
        primary: field.Key === 'PRI',
        unique: field.Key === 'UNI',
        autoIncrement: field.Extra === 'auto_increment',
      }));

      const primaryKeys = struct.filter(({ primary }) => primary);

      return {
        tableName,
        primaryKey: primaryKeys.length === 1 ? primaryKeys[0] : undefined,
        pascalTableName: pascalCase(tableName),
        camelTableName: camelCase(tableName),
        seqTypeList: [...new Set(struct.map(({ seqType }) => seqType))],
        struct,
      };
    }),
  );

  // 生成 model
  const modelTemplate = (
    await fs.readFile(join(__dirname, './templates/model.txt'))
  ).toString();

  const renderModel = ejs.compile(modelTemplate, {});

  await fs.mkdir(join(config.appRootPath, 'model'), { recursive: true });
  for (let i = 0; i < tableStruct.length; i++) {
    const struct = tableStruct[i];
    if (modelSet.has(struct.tableName)) {
      await fs.writeFile(
        join(config.appRootPath, `model/${struct.camelTableName}.ts`),
        renderModel({ ...struct, camelCase, pascalCase }),
      );
    }
  }

  // 生成 service
  const serviceTemplate = (
    await fs.readFile(join(__dirname, './templates/service.txt'))
  ).toString();

  const renderService = ejs.compile(serviceTemplate, {});

  await fs.mkdir(join(config.appRootPath, 'service'), { recursive: true });
  for (let i = 0; i < tableStruct.length; i++) {
    const struct = tableStruct[i];
    if (serviceSet.has(struct.tableName)) {
      await fs.writeFile(
        join(
          config.appRootPath,
          `service/${struct.pascalTableName}BaseService.ts`,
        ),
        renderService({ ...struct, camelCase, pascalCase }),
      );
    }
  }

  // 生成 controller
  const controllerTemplate = (
    await fs.readFile(join(__dirname, './templates/controller.txt'))
  ).toString();

  const renderController = ejs.compile(controllerTemplate, {});

  await fs.mkdir(join(config.appRootPath, 'controller'), { recursive: true });
  for (let i = 0; i < tableStruct.length; i++) {
    const struct = tableStruct[i];
    if (controllerSet.has(struct.tableName)) {
      await fs.writeFile(
        join(
          config.appRootPath,
          `controller/${struct.pascalTableName}BaseController.ts`,
        ),
        renderController({ ...struct, camelCase, pascalCase }),
      );
    }
  }

  connection.destroy();
})();
